/**
 * FriendFeed AS3 API
 * 
 * For more information: http://code.google.com/p/friendfeed-as3/
 * This script is based on the great twitter API TwitterScript (http://code.google.com/p/twitterscript/)
 * 
 * Todo:
 * 
 *  - A Search class like the one in TwitterScript
 *  - Publish binary images in multipart/form-data
 *  - Update user profile information (name and multipart/form-data picture)
 *  - Methods for Subscription Management
 *  - Methods for Real-time
 * 
 * P.S. version date format: dd/mm/yyyy
 * 
 * @author	Lucas Araujo <araujo.lucas [at] gmail.com> - http://FriendFeed.com/LucasAraujo
 * @version	0.6 10/2/2009 19:42
 */
package friendfeed.api 
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.net.URLRequestHeader;
	import flash.net.URLVariables;
	import flash.net.FileReference;
	
	import friendfeed.api.events.FriendFeedEvent;
	import friendfeed.api.data.Entry;
	import friendfeed.api.data.Profile;

	import com.adobe.serialization.json.JSON;
	import com.dynamicflash.util.Base64;
	
	/**
	 * FriendFeed API Class
	 * 
	 * Usage:
	 * <code>
	 * var friendFeed:FriendFeed = new FriendFeed("nickname", "remotekey");
	 * friendFeed.addEventListener(FriendFeedEvent.ON_PUBLIC_FEED_RESULT, function(e:FriendFeedEvent):void
	 * {
	 *     trace("Number of Entries:", e.data.length);
	 *     for (var i:String in e.data)
	 *     {
	 *        var entry:Entry = (e.data[i] as Entry);
	 *        trace(entry.user.name, entry.title, entry.published);
	 *     }
	 * });
	 * friendFeed.loadPublicFeed();
	 * </code>
	 */
	public class FriendFeed extends EventDispatcher
	{
		// constants used for loaders
		protected static const PUBLIC_FEED:String = "publicFeed";
		protected static const USER_FEED:String = "userFeed";
		protected static const USER_COMMENTS_FEED:String = "userCommentsFeed";
		protected static const USER_LIKES_FEED:String = "userLikesFeed";
		protected static const USER_DISCUSSION_FEED:String = "userDiscussionFeed";
		protected static const USER_FRIENDS_FEED:String = "userFriendsFeed";
		protected static const USER_ROOMS_FEED:String = "userRoomsFeed";
		protected static const ROOM_FEED:String = "roomFeed";
		protected static const ENTRY_FEED:String = "entryFeed";		
		protected static const HOME_FEED:String = "homeFeed";
		protected static const SEARCH_FEED:String = "searchFeed";
		protected static const LIST_FEED:String = "listFeed";
		protected static const URL_FEED:String = "urlFeed";
		protected static const DOMAIN_FEED:String = "domainFeed";		
		protected static const SHARE:String = "share";
		protected static const SET_COMMENT:String = "setComment";
		protected static const DELETE_COMMENT:String = "deleteComment";
		protected static const SET_LIKE:String = "setLike";
		protected static const DELETE_LIKE:String = "deleteLike";
		protected static const DELETE_ENTRY:String = "deleteEntry";
		protected static const HIDE_ENTRY:String = "hideEntry";
		protected static const PROFILE:String = "profile";
		protected static const SET_PROFILE:String = "setProfile";
		protected static const ROOM_PROFILE:String = "roomProfile";
		protected static const LIST_PROFILE:String = "listProfile";
		protected static const VALIDATE:String = "validate";
		protected static const SERVICES:String = "services";
		
		// Public data format constants
		public static const JSON_FORMAT:String = "json";
		public static const XML_FORMAT:String = "xml";
		public static const RSS_FORMAT:String = "rss";
		public static const ATOM_FORMAT:String = "atom";
		
		// Public image size for user picture and room picture
		public static const SMALL_SIZE:String = "small";
		public static const MEDIUM_SIZE:String = "medium";
		public static const LARGE_SIZE:String = "large";
		
		// All of the API requests that output feeds are available in four formats
		public static const AVAILABLE_FORMATS:Array = [JSON_FORMAT, XML_FORMAT, RSS_FORMAT, ATOM_FORMAT];
		
		// Room picture or user picture image sizes
		public static const AVAILABLE_SIZES:Array = [SMALL_SIZE, MEDIUM_SIZE, LARGE_SIZE];

		// Use secure connection?
		public var useHttps:Boolean = false;

		// Last url used (for debug only)
		public var lastURL:String;
		
		
		// internal variables
		
		// URLLoders
		protected var loaders:Array;

		// Format - Json is the only format that returns a Entry object
		// Use: ffObject.format = JSON_FORMAT;
		protected var dataFormat:String = JSON_FORMAT;

		// for auth
		protected var authorizationHeader:URLRequestHeader;
		
		// Api key (for Subscription Management)
		// An API key is a private key that developers can obtain by sending an email to api [at] friendfeed.com
		// Use: ffObject.apikey = "[YOUR API KEY]";
		protected var _apikey:String;
		
		// FriendFeed URL
		protected var friendFeedUrl:String = "http://friendfeed.com";

		/**
		 * Constructs a FriendFeed session with the given authentication params.
		 * 
		 * @param	nickname
		 * @param	remoteKey
		 */
		public function FriendFeed(nickname:String=null, remoteKey:String=null) 
		{
			this.setAuthenticationCredentials(nickname, remoteKey);
		}

		/**
		 * Set nickname and remote key (See http://friendfeed.com/remotekey)
		 * 
		 * @param	nickname
		 * @param	remoteKey
		 */
		public function setAuthenticationCredentials(nickname:String, remoteKey:String):void 
		{
			if (null != nickname && null != remoteKey)
			{
		 		var credentials:String = nickname + ":" + remoteKey;
		 		var encodedCredentials:String = Base64.encode(credentials);
		 		authorizationHeader = new URLRequestHeader("Authorization", "Basic " + encodedCredentials);
			}			
		}
		
		
		// Public API
	
		/**
		 * Returns the public feed with everyone's public entries.
		 * Authentication is not required.
		 * 
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadPublicFeed(service:String=null, start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(PUBLIC_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/public", urlParams);			
			loader.load(request);
		}
		
		/**
		 * Fetch Entries from a User
		 * If the user has a private feed, authentication is required.
		 * 
		 * @param	nickname	Can be the user nickname ("bret") or an array of names (["bret", "paul", "jim"])
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadUserFeed(nickname:*, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var url:String;
			if (nickname is String)
			{
				url = "/api/feed/user/" + nickname;
			} 
			else if (nickname is Array)
			{
				url = "/api/feed/user";
				urlParams.nickname = nickname.join(",");
			} 
			else
				throw new Error("Invalid nickname type. Only String and Array are acceptable.");
			
			var loader:URLLoader = this.getLoader(USER_FEED);			
			var request:URLRequest = this.friendFeedRequest(url, urlParams);			
			loader.load(request);
		}

		/**
		 * Fetch Entries a User Has Commented On
		 * If the user has a private feed, authentication is required.
		 * 
		 * @param	nickname	the user nickname ("bret")
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadUserCommentsFeed(nickname:String, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(USER_COMMENTS_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/user/"+ nickname +"/comments", urlParams);			
			loader.load(request);
		}

		/**
		 * Fetch Entries a User Has "Liked"
		 * If the user has a private feed, authentication is required.
		 * 
		 * @param	nickname	the user nickname ("bret")
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadUserLikesFeed(nickname:String, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(USER_LIKES_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/user/"+ nickname +"/likes", urlParams);			
			loader.load(request);
		}

		/**
		 * Fetch Entries a User Has Commented On or "Liked"
		 * If the user has a private feed, authentication is required.
		 * 
		 * @param	nickname	the user nickname ("bret")
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadUserDiscussionFeed(nickname:String, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(USER_DISCUSSION_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/user/"+ nickname +"/discussion", urlParams);			
			loader.load(request);
		}

		/**
		 * Fetch Entries from a User's Friends Feed
		 * If the user has a private feed, authentication is required. 
		 * Any friends of the user who have a private feed will be omitted unless authentication is 
		 * used and the authenticated user has the privileged to see those feeds.
		 * 
		 * @param	nickname	the user nickname ("bret")
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadUserFriendsFeed(nickname:String, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(USER_FRIENDS_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/user/"+ nickname +"/friends", urlParams);			
			loader.load(request);
		}
		
		/**
		 * Fetch the Rooms Feed
		 * Returns the entries the authenticated user would see on their Rooms page - entries from all of the rooms 
		 * they are members of.
		 * Authentication is always required. 
		 * 
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadUserRoomsFeed(service:String=null, start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(USER_ROOMS_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/rooms", urlParams);			
			loader.load(request);
		}
		
		/**
		 * Fetch Entries from a Room
		 * If the room is private, authentication is required. 
		 * 
		 * @param	room		the room nickname
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadRoomFeed(room:String, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(ROOM_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/room/"+ room, urlParams);			
			loader.load(request);
		}
		
		/**
		 * Fetch Entry by id or fetch Multiple Entries by ID
		 * Authentication is not required, but private entries will be omitted from unauthenticated results, 
		 * as will entries the authenticated user does not have permission to view. 
		 * 
		 * @param	room		the room nickname
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadEntryFeed(entryID:*, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var url:String;

			if (entryID is String)
			{
				url = "/api/feed/entry/" + entryID;
			} 
			else if (entryID is Array)
			{
				url = "/api/feed/entry";
				urlParams.entry_id = entryID.join(",");
			} 
			else
				throw new Error("Invalid entryID type. Only String and Array are acceptable.");

			var loader:URLLoader = this.getLoader(ENTRY_FEED);			
			var request:URLRequest = this.friendFeedRequest(url, urlParams);			
			loader.load(request);
		}

		/**
		 * Returns the entries the authenticated user sees on their home page.
		 * Authentication is always required.
		 *
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadHomeFeed(service:String=null, start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(HOME_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/home", urlParams);			
			loader.load(request);
		}
		
		/**
		 * Executes a search over the entries in FriendFeed. The default scope is over all public entries. 
		 * The query syntax is the same syntax as http://friendfeed.com/search/advanced.
		 *
		 * @param	query		search term
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function search(query:String, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(SEARCH_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			urlParams.q = escape(query);			
			var request:URLRequest = this.friendFeedRequest("/api/feed/search", urlParams);			
			loader.load(request);
		}
		
		/**
		 * Returns entries from the authenticated users list with the given nickname
		 * Authentication is always required. 
		 *
		 * @param	list		list nickname
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadListFeed(list:String, service:String=null, 
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(LIST_FEED);
			var urlParams:Object = this.getURLParams(service, start, num, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/feed/list/"+ list, urlParams);			
			loader.load(request);
		}
		
		/**
		 * Returns the most recent entries linking to a given URL. 
		 * If authentication is used, private entries may be returned, otherwise only public entries will be. 
		 * 
		 * P.S. (from FriendFeed) The service filter is not currently supported on this feed.
		 *
		 * @param	url			
		 * @param	nicknames	The results may also be limited to a given set of users or rooms by specifying their nicknames
		 * @param	subscribed	
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadUrlFeed(url:String, nicknames:Array = null, subscribed:Boolean=false,
			start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(URL_FEED);
			var urlParams:Object = this.getURLParams(null, start, num, includeArg);
			urlParams.url = url;
			if (nicknames)
				urlParams.nickname = nicknames.join(",");
			if (subscribed)
				urlParams.subscribed = 1;
			var request:URLRequest = this.friendFeedRequest("/api/feed/url", urlParams);			
			loader.load(request);
		}
		
		/**
		 * Returns the most recent entries linking to URLs in the given domain(s). 
		 * If authentication is used, private entries may be returned, otherwise only public entries will be. 
		 * 
		 * P.S. The service filter is not currently supported on this feed.
		 *
		 * @param	domain		A string or an array of domains
		 * @param	nicknames	The results may also be limited to a given set of users or rooms by specifying their nicknames
		 * @param	subscribed	
		 * @param 	inexact		Subdomain matches may be requested by passing inexact=1
		 * @param	service		only return entries from the service with the given ID, e.g., service=twitter
		 * @param	start		return entries starting with the given index, e.g., start=30
		 * @param	num			return num entries starting from start, e.g., num=10
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadDomainFeed(domain:*, nicknames:Array = null, subscribed:Boolean = false,
			inexact:Boolean = false, start:Number=0, num:Number=0, includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(DOMAIN_FEED);
			var urlParams:Object = this.getURLParams(null, start, num, includeArg);

			if (domain is String)
			{
				urlParams.domain = domain;
			} 
			else if (domain is Array)
			{
				urlParams.domain = domain.join(",");
			} 
			else
				throw new Error("Invalid entryID type. Only String and Array are acceptable.");

			if (nicknames)
				urlParams.nickname = nicknames.join(",");
			if (subscribed)
				urlParams.subscribed = 1;
			if (inexact)
				urlParams.inexact = 1;
			var request:URLRequest = this.friendFeedRequest("/api/feed/domain", urlParams);			
			loader.load(request);
		}

		/**
		 * Create New Entries
		 * 
		 * There are two forms to use this method, the simpler one:
		 * <code>
		 * friendFedd.share("A message");
		 * friendFedd.share("A message and a link", "http://friendfeed.com/");
		 * friendFedd.share("A message, a link and a comment", "http://friendfeed.com/", "This is a comment");
		 * </code>
		 * 
		 * Or a more complex, used to post files and images:
		 * <code>
		 * var entry:FriendFeedEntry = new FriendFeedEntry("A complex entry");
		 * // ... some code (see FriendFeedEntry class) for more information
		 * friendFedd.share(entry);
		 * </code>
		 * 
		 * @param	data	can be a message (a String) or a FriendFeedEntry object
		 * @param	link	(optional) if data is a String, you can set a link to this entry
		 * @param	comment	(optional) if data is a String, you can set a comment to this entry
		 * 
		 * @see	FriendFeedEntry class for more information
		 */
		public function share(data:*, link:String=null, comment:String=null, room:String=null):void
		{
			var entry:Entry = Entry.factory(data, link, comment, room);
			if (entry)
			{
				var loader:URLLoader = this.getLoader(SHARE);
				var request:URLRequest = this.friendFeedRequest("/api/share", null, entry.toURLVariables());			
				loader.load(request);
			} 
			else
				throw new Error("Invalid type");
		}

		/**
		 * This will add a comment or edit an existing comment on a FriendFeed entry
		 * 
		 * @param	entry	
		 * @param	body	
		 * @param	comment	
		 */
		public function setComment(entry:String, body:String, comment:String=null):void
		{
			var loader:URLLoader = this.getLoader(SET_COMMENT);
			var postParams:Object = { };
			postParams.entry = entry;
			postParams.body = body;
			if (comment)
				postParams.comment = comment;
			var request:URLRequest = this.friendFeedRequest("/api/comment", null, postParams);
			loader.load(request);
		}

		/**
		 * 
		 * @param	entry		The FriendFeed UUID of the entry to which this comment is attached. 
		 * @param	comment		
		 * @param	undelete	
		 */
		public function deleteComment(entry:String, comment:String, undelete:Boolean=false):void
		{
			var loader:URLLoader = this.getLoader(DELETE_COMMENT);
			var postParams:Object = { };
			postParams.entry = entry;
			postParams.comment = comment;
			if (undelete)
				postParams.undelete = 1;
			var request:URLRequest = this.friendFeedRequest("/api/comment/delete", null, postParams);
			loader.load(request);
		}
		
		/**
		 * This will add a "Like" to a FriendFeed entry for the authenticated user. 
		 * 
		 * @param	entry	The FriendFeed UUID of the entry to which this "like" is attached. 
		 * @param	delete
		 */
		public function like(entry:String, deleteThisLike:Boolean = false):void
		{
			if (deleteThisLike)
				return this.deleteLike(entry);
			
			var loader:URLLoader = this.getLoader(SET_LIKE);
			var postParams:Object = { };
			postParams.entry = entry;
			var request:URLRequest = this.friendFeedRequest("/api/like", null, postParams);
			loader.load(request);
		}
		
		/**
		 * This will delete an existing "Like"
		 * 
		 * @param	entry	The FriendFeed UUID of the entry to which this "like" is attached. 
		 */
		public function deleteLike(entry:String):void
		{
			var loader:URLLoader = this.getLoader(DELETE_LIKE);
			var postParams:Object = { };
			postParams.entry = entry;
			var request:URLRequest = this.friendFeedRequest("/api/like/delete", null, postParams);
			loader.load(request);
		}
		
		/**
		 * This will delete an existing entry
		 * 
		 * @param	entry		The FriendFeed UUID of the entry
		 * @param	undelete	(optional) if given, un-delete the given entry if it is already deleted 
		 */
		public function deleteEntry(entry:String, undelete:Boolean=false):void
		{
			var loader:URLLoader = this.getLoader(DELETE_ENTRY);
			var postParams:Object = { };
			postParams.entry = entry;
			if (undelete)
				postParams.undelete = 1;
			var request:URLRequest = this.friendFeedRequest("/api/entry/delete", null, postParams);
			loader.load(request);			
		}
		
		/**
		 * This will hide an existing entry
		 * 
		 * @param	entry		The FriendFeed UUID of the entry
		 * @param	unhide	(optional) if given, un-hide the given entry if it is already hidden 
		 */
		public function hideEntry(entry:String, unhide:Boolean=false):void
		{
			var loader:URLLoader = this.getLoader(HIDE_ENTRY);
			var postParams:Object = { };
			postParams.entry = entry;
			if (unhide)
				postParams.unhide = 1;
			var request:URLRequest = this.friendFeedRequest("/api/entry/hide", null, postParams);
			loader.load(request);			
		}
		
		/**
		 * Get a profile or multiple profiles
		 * 
		 * @param	nickname
		 */
		public function loadProfile(nickname:*):void
		{
			var loader:URLLoader = this.getLoader(PROFILE);
			var url:String;
			var urlParams:Object = null;

			if (nickname is String)
			{
				url = "/api/user/"+ nickname +"/profile";
			}
			else if (nickname is Array)
			{
				url = "/api/profiles";
				urlParams = new Object();
				urlParams.nickname = nickname.join(",");
			}
			var request:URLRequest = this.friendFeedRequest(url, urlParams);			
			loader.load(request);
		}
		
		/**
		 * 
		 * @param	nickname
		 * @param	profile
		 */
		public function setProfile(nickname:String, profile:friendfeed.api.data.Profile):void
		{
			var loader:URLLoader = this.getLoader(SET_PROFILE);
			var post_args:URLVariables = profile.getURLVariables();
			var url:String = "/api/user/"+ nickname +"/profile";
			var request:URLRequest = this.friendFeedRequest(url, null, post_args);			
			loader.load(request);
		}
		
		/**
		 * Returns a list of all of the room's members and the url associated with the room
		 * Authentication required for private rooms
		 * 
		 * @param	nickname
		 */
		public function loadRoomProfile(nickname:String):void
		{
			var loader:URLLoader = this.getLoader(ROOM_PROFILE);
			var request:URLRequest = this.friendFeedRequest("/api/room/"+ nickname +"/profile");			
			loader.load(request);
		}
		
		/**
		 * Returns a list of all of the list's members and the url associated with the list
		 * Authentication required
		 * 
		 * @param	nickname
		 */
		public function loadListProfile(nickname:String):void
		{
			var loader:URLLoader = this.getLoader(ROOM_PROFILE);
			var request:URLRequest = this.friendFeedRequest("/api/list/"+ nickname +"/profile");			
			loader.load(request);
		}
		
		/**
		 * Validate user
		 * 
		 * @param	nickname
		 * @param	remoteKey
		 */
		public function validate(nickname:String, remoteKey:String):void
		{
			this.setAuthenticationCredentials(nickname, remoteKey);
			var loader:URLLoader = this.getLoader(VALIDATE);
			var request:URLRequest = this.friendFeedRequest("/api/validate");
			loader.load(request);	
		}

		/**
		 * Load a list of all services currently supported by FriendFeed
		 * 
		 * @param	includeArg	return a subset of data, e.g., include=id,services
		 */
		public function loadServices(includeArg:Array=null):void
		{
			var loader:URLLoader = this.getLoader(SERVICES);
			var urlParams:Object = this.getURLParams(null, 0, 0, includeArg);
			var request:URLRequest = this.friendFeedRequest("/api/services", urlParams);
			loader.load(request);
		}
		
		
		// protected handlers for the events coming back from FriendFeed

		protected function publicFeedHandler(e:Event):void
		{
			this.defaultHandler(PUBLIC_FEED, FriendFeedEvent.ON_PUBLIC_FEED_RESULT);
		}
		
		protected function userFeedHandler(e:Event):void
		{
			this.defaultHandler(USER_FEED, FriendFeedEvent.ON_USER_FEED_RESULT);
		}
		
		protected function userCommentsHandler(e:Event):void
		{
			this.defaultHandler(USER_COMMENTS_FEED, FriendFeedEvent.ON_USER_COMMENTS_RESULT);
		}
		
		protected function userLikesHandler(e:Event):void
		{
			this.defaultHandler(USER_LIKES_FEED, FriendFeedEvent.ON_USER_LIKES_RESULT);
		}
		
		protected function userDiscussionHandler(e:Event):void
		{
			this.defaultHandler(USER_DISCUSSION_FEED, FriendFeedEvent.ON_USER_DISCUSSION_RESULT);
		}
		
		protected function userFriendsHandler(e:Event):void
		{
			this.defaultHandler(USER_FRIENDS_FEED, FriendFeedEvent.ON_USER_FRIENDS_RESULT);
		}
	
		protected function userRoomsHandler(e:Event):void
		{
			this.defaultHandler(USER_ROOMS_FEED, FriendFeedEvent.ON_USER_ROOMS_RESULT);
		}
		
		protected function roomFeedHandler(e:Event):void
		{
			this.defaultHandler(ROOM_FEED, FriendFeedEvent.ON_ROOM_FEED_RESULT);
		}
		
		protected function entryFeedHandler(e:Event):void
		{
			this.defaultHandler(ENTRY_FEED, FriendFeedEvent.ON_ENTRY_FEED_RESULT);
		}

		protected function homeFeedHandler(e:Event):void
		{	
			this.defaultHandler(HOME_FEED, FriendFeedEvent.ON_HOME_FEED_RESULT);	
		}
		
		protected function searchFeedHandler(e:Event):void
		{
			this.defaultHandler(SEARCH_FEED, FriendFeedEvent.ON_SEARCH_RESULT);
		}
		
		protected function listFeedHandler(e:Event):void
		{
			this.defaultHandler(LIST_FEED, FriendFeedEvent.ON_LIST_FEED_RESULT);
		}
		
		protected function urlFeedHandler(e:Event):void
		{
			this.defaultHandler(URL_FEED, FriendFeedEvent.ON_URL_FEED_RESULT);
		}
		
		protected function domainFeedHandler(e:Event):void
		{
			this.defaultHandler(DOMAIN_FEED, FriendFeedEvent.ON_DOMAIN_FEED_RESULT);
		}
		
		protected function shareHandler(e:Event):void
		{
			this.defaultHandler(SHARE, FriendFeedEvent.ON_SHARE_RESULT);
		}
		
		protected function commentHandler(e:Event):void
		{
			this.defaultHandler(SET_COMMENT, FriendFeedEvent.ON_SET_COMMENT_RESULT);
		}
		
		protected function deleteCommentHandler(e:Event):void
		{
			this.defaultHandler(DELETE_COMMENT, FriendFeedEvent.ON_DELETE_COMMENT_RESULT);
		}
		
		protected function likeHandler(e:Event):void
		{
			this.defaultHandler(SET_LIKE, FriendFeedEvent.ON_SET_LIKE_RESULT);
		}
		
		protected function deleteLikeHandler(e:Event):void
		{
			this.defaultHandler(DELETE_LIKE, FriendFeedEvent.ON_DELETE_LIKE_RESULT);
		}
		
		protected function deleteEntryHandler(e:Event):void
		{
			this.defaultHandler(DELETE_ENTRY, FriendFeedEvent.ON_DELETE_ENTRY_RESULT);
		}
		
		protected function hideEntryHandler(e:Event):void
		{
			this.defaultHandler(HIDE_ENTRY, FriendFeedEvent.ON_HIDE_ENTRY_RESULT);
		}
		
		/**
		 * Profile Handler
		 * 
		 * @param	e	An event
		 */
		protected function profileHandler(e:Event):void
		{
			this.defaultProfileHandler(PROFILE, FriendFeedEvent.ON_PROFILE_RESULT);
		}
		
		protected function setProfileHandler(e:Event):void
		{
			this.defaultProfileHandler(SET_PROFILE, FriendFeedEvent.ON_SET_PROFILE_RESULT);
		}
		
		protected function roomProfileHandler(e:Event):void
		{
			this.defaultHandler(ROOM_PROFILE, FriendFeedEvent.ON_ROOM_PROFILE_RESULT);
		}
		
		protected function listProfileHandler(e:Event):void
		{
			this.defaultHandler(LIST_PROFILE, FriendFeedEvent.ON_LIST_PROFILE_RESULT);
		}
		
		protected function validateHandler(e:Event):void
		{
			this.defaultHandler(VALIDATE, FriendFeedEvent.ON_VALIDATE_RESULT);
		}
		
		protected function servicesHandler(e:Event):void
		{
			this.defaultHandler(SERVICES, FriendFeedEvent.ON_SERVICES_RESULT);
		}
		
		
		// protected helper handlers
		
		/**
		 * This method dipatch a new FriendFeedEvent 
		 * 
		 * @param	errorEvent
		 */
		protected function errorHandler(errorEvent:IOErrorEvent):void
		{
			var t:FriendFeedEvent = new FriendFeedEvent(FriendFeedEvent.ON_ERROR);
			t.data = errorEvent.text;
			this.dispatchEvent(t);
		}
		
		/**
		 * Default Handler
		 * If the result is a json string and it has the entries propertie, it'll returns a
		 * array of "frindfeed.api.data.Entry" Objects
		 * 
		 * @param	loaderName
		 * @param	eventName
		 */
		protected function defaultHandler(loaderName:String, eventName:String):void
		{
			var data:String = this.getLoader(loaderName).data;
			var result:FriendFeedEvent = new FriendFeedEvent(eventName);
			switch(this.dataFormat)
			{
				// json format
				case JSON_FORMAT:
					var jsonData:Object = JSON.decode(data);
					// Is a entry
					if (jsonData["entries"])
					{
						var resultArray:Array = [];
						var length:uint = jsonData.entries.length;
						for (var i:uint = 0; i < length; i++)
						{
							var entry:friendfeed.api.data.Entry = 
								friendfeed.api.data.Entry.newFromJSON(jsonData.entries[i]);
							resultArray.push(entry);
						}
						result.data = resultArray;
					}
					// Is another object, like profile, services
					else
						result.data = jsonData;
				break;
				// xml, rss and atom
				default:
					result.data = new XML(data);
				break;
			}
			this.dispatchEvent(result);
		}
		
		/**
		 * Default Profile Handler
		 * 
		 * @param	loaderName
		 * @param	eventName
		 */
		protected function defaultProfileHandler(loaderName:String, eventName:String):void
		{
			var data:String = this.getLoader(loaderName).data;
			var result:FriendFeedEvent = new FriendFeedEvent(eventName);
			switch(this.dataFormat)
			{
				// json format
				case JSON_FORMAT:
					var jsonData:Object = JSON.decode(data);
					// Is a array of profile
					if (jsonData["profiles"])
					{
						var resultArray:Array = [];
						var length:uint = jsonData.profiles.length;
						for (var i:uint = 0; i < length; i++)
						{
							var profile:friendfeed.api.data.Profile = 
								friendfeed.api.data.Profile.newFromJSON(jsonData.profiles[i]);
							resultArray.push(profile);							
						}
						result.data = resultArray;					
					}
					else
						result.data = friendfeed.api.data.Profile.newFromJSON(jsonData);						
				break;
				// xml, rss and atom
				default:
					result.data = new XML(data);
				break;
			}
			this.dispatchEvent(result);
		}
		
		
		
		// Miscellaneous Methods
		
		/**
		 * Get a user's profile picture (url)
		 * 
		 * @param	nickname
		 * @param	size
		 * @return	An URL
		 */
		public function getUserPictureURL(nickname:String, size:String=SMALL_SIZE):String
		{			
			if (nickname == null)
				throw new Error("You must specify a nickname");
			
			if (size == null)
				size = SMALL_SIZE;
			
			if (AVAILABLE_SIZES.indexOf(size) == -1)
				throw new Error("Invalid image size");
			return this.friendFeedUrl +"/" + nickname +"/picture?size=" + size;
		}

		/**
		 * Get a rooms's profile picture
		 * 
		 * @param	room
		 * @param	size
		 * @return	An URL
		 */
		public function getRoomPictureURL(room:String, size:String=SMALL_SIZE):String
		{
			if (AVAILABLE_SIZES.indexOf(size) == -1)
				throw new Error("Invalid image size");
			return this.friendFeedUrl +"/rooms/" + room +"/picture?size=" + size;
		}
		
		// Setters and Getters
		
		/**
		 * 
		 */
		public function set format(value:String):void
		{
			if (AVAILABLE_FORMATS.indexOf(value) != -1)
				this.dataFormat = value;
			else
				throw new Error("Invalid format");
		}
		
		/**
		 * 
		 */
		public function get format():String
		{
			return this.dataFormat;
		}
		
		/**
		 * 
		 */
		public function set apikey(value:String):void
		{
			this._apikey = value;
		}
		
		/**
		 * 
		 */
		public function get apikey():String
		{
			return this._apikey;
		}
		
		
		// protected helper methods
		
		/**
		 * 
		 * @param	name
		 * @param	completeHandler
		 */
		protected function addLoader(name:String, completeHandler:Function):void
		{
			var loader:URLLoader = new URLLoader();
			loader.addEventListener(Event.COMPLETE, completeHandler);
			loader.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, errorHandler);
			this.loaders[name] = loader;
		}
		
		/**
		 * 
		 * @param	name
		 * @return
		 */
		protected function getLoader(name:String):URLLoader
		{
			if (this.loaders == null)
			{
				this.loaders = [];
				this.addLoader(PUBLIC_FEED, publicFeedHandler);
				this.addLoader(USER_FEED, userFeedHandler);
				this.addLoader(USER_COMMENTS_FEED, userCommentsHandler);
				this.addLoader(USER_LIKES_FEED, userLikesHandler);
				this.addLoader(USER_DISCUSSION_FEED, userDiscussionHandler);
				this.addLoader(USER_FRIENDS_FEED, userFriendsHandler);
				this.addLoader(USER_ROOMS_FEED, userRoomsHandler);
				this.addLoader(ROOM_FEED, roomFeedHandler);
				this.addLoader(ENTRY_FEED, entryFeedHandler);
				this.addLoader(HOME_FEED, homeFeedHandler);
				this.addLoader(SEARCH_FEED, searchFeedHandler);
				this.addLoader(LIST_FEED, listFeedHandler);
				this.addLoader(URL_FEED, urlFeedHandler);
				this.addLoader(DOMAIN_FEED, domainFeedHandler);	
				this.addLoader(SHARE, shareHandler);
				this.addLoader(SET_COMMENT, commentHandler);
				this.addLoader(DELETE_COMMENT, deleteCommentHandler);
				this.addLoader(SET_LIKE, likeHandler);
				this.addLoader(DELETE_LIKE, deleteLikeHandler);
				this.addLoader(DELETE_ENTRY, deleteEntryHandler);
				this.addLoader(HIDE_ENTRY, hideEntryHandler);
				this.addLoader(PROFILE, profileHandler);
				this.addLoader(SET_PROFILE, setProfileHandler);
				this.addLoader(ROOM_PROFILE, roomProfileHandler);
				this.addLoader(LIST_PROFILE, listProfileHandler);
				this.addLoader(VALIDATE, validateHandler);
				this.addLoader(SERVICES, servicesHandler);
			}
			return this.loaders[name] as URLLoader;
		}

		/**
		 * 
		 * @param	service
		 * @param	start
		 * @param	num
		 * @param	includeArg
		 * @return	An object that will be transformed in a "pair=value" string
		 */
		protected function getURLParams(service:String=null, start:Number=0, num:Number=0, includeArg:Array=null):Object
		{
			var result:Object = { service:service, start:start, num:num  };
			if (null != includeArg)
				result["include"] = includeArg.join(",");
			return result;
		}
		
		/**
		 * 
		 * @param	url_args
		 * @return	A query string from url_args, with format and apikey params, if any.
		 */
		protected function getQueryString(url_args:Object = null):String
		{
			var pairs:Array = [];
			for (var i:String in url_args)
				if (url_args[i])
					pairs.push(i + "=" + url_args[i]);
			if (this.dataFormat != JSON_FORMAT)
				pairs.push('format=' + this.dataFormat);
			if (this.apikey)
				pairs.push('apikey=' + this.apikey);
			
			if (pairs.length)
				return "?" + pairs.join("&");
			return "";
		}
		
		/**
		 * 
		 * @param	uri
		 * @param	url_args
		 * @return	An URL
		 */
		protected function getUrl(uri:String, url_args:Object = null):String
		{
			var query:String = this.getQueryString(url_args);
			var url:String = this.friendFeedUrl + uri + query;
			if (this.useHttps)
				url = url.replace("http://", "https://");
			return url;
		}
		
		/**
		 * 
		 * @param	uri
		 * @param	url_args
		 * @param	post_args
		 * @return
		 */
		protected function friendFeedRequest(uri:String, url_args:Object=null, post_args:*=null):URLRequest
		{
			var url:String = this.getUrl(uri, url_args);
			var request:URLRequest = new URLRequest(url);
			if (this.authorizationHeader)
				request.requestHeaders = [this.authorizationHeader];

			if (null != post_args)
			{
				var variables:URLVariables;
				if (post_args is URLVariables)
				{
					variables = (post_args as URLVariables);
				}
				else if (post_args is Object)
				{
					variables = new URLVariables();
					for (var p:String in post_args)
							variables[p] = post_args[p];
				}
				else
					throw new Error("Invalid POST object");
				request.method = URLRequestMethod.POST;
				request.data = variables;
			}

			this.lastURL = url;
			return request;
		}
	}
}